/*
 * File     : Utf8Coder.java
 * Created  : 10 Nov 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.string;

import static com.googlecode.dni.internal.MemoryAccess.MEMORY_ACCESSOR;

import java.nio.charset.Charset;

import com.googlecode.dni.internal.DniTypes;
import com.googlecode.dni.internal.NativeUnsafeAccess;

/**
 * <p>
 *  Provides a faster UTF-8 encoding than that in the default platform.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class Utf8Coder
{

    /**
     * Threshold of string length above which strings are processed in native
     * code.  This value was found empirically.
     */
    private static final int NATIVE_THRESHOLD = 12;


    /** Prevents instantiation. */
    private Utf8Coder()
    {
        throw new UnsupportedOperationException();
    }

    /**
     * Obtains an upper bound on the space required for the given string with a
     * null terminator.
     *
     * @param string
     *            the string
     * @return the size in bytes
     */
    public static int getByteCount( final String string )
    {
        // small strings are unimportant
        if ( string.length() < 256 )
        {
            return string.length() * 3 + 1;
        }

        // for large strings, we can't overestimate so much
        int size = 0;
        for ( int i = 0; i < string.length(); i++ )
        {
            char c = string.charAt( i );
            if ( c < 0x0080 )
            {
                size++;
            }
            else if ( c < 0x0800 )
            {
                size += 2;
            }
            else if ( ( c & 0xf800 ) != 0xd800 )
            {
                size += 3;
            }
            else
            {
                size += 4;
                i++;
            }
        }

        return size;
    }

    /**
     * Encodes the given string into the given buffer.  The position is then
     * undefined.
     *
     * @param string
     *            the string
     * @param pointer
     *            the pointer
     *
     * @return the number of bytes used (including terminator)
     */
    public static int encode( final String string,
                              final long pointer )
    {
        if ( string.length() > NATIVE_THRESHOLD )
        {
            return NativeUnsafeAccess.writeUtf8String( string, pointer );
        }

        long cursor = pointer;

        for ( int i = 0; i < string.length(); i++ )
        {
            char c = string.charAt( i );
            if ( c < 0x0080 )
            {
                MEMORY_ACCESSOR.putByte( cursor++, (byte) c );
            }
            else if ( c < 0x800 )
            {
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0xc0 | ( ( c >> 6 ) & 0x1f ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( c & 0x3f ) ) );
            }
            else if ( ( c & 0xfc00 ) == 0xd800 )
            {
                // high surrogate; next should be low surrogate
                if ( i == string.length() - 1 )
                {
                    assert false : "Malformed string: " + string;
                    break;
                }
                char next = string.charAt( i + 1 );
                if ( ( next & 0xfc00 ) != 0xdc00 )
                {
                    assert false : "Malformed string: " + string;
                    break;
                }

                int v = 0x10000 + ( ( next & 0x3ff ) | ( ( c & 0x3ff ) << 10 ) );

                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0xf0 | ( ( v >> 18 ) & 0x07 ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( ( v >> 12 ) & 0x3f ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( ( v >> 6 ) & 0x3f ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( v & 0x3f ) ) );

                i++;
            }
            else
            {
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0xe0 | ( ( c >> 12 ) & 0x0f ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( ( c >> 6 ) & 0x3f ) ) );
                MEMORY_ACCESSOR.putByte( cursor++, (byte) ( 0x80 | ( c & 0x3f ) ) );
            }
        }

        MEMORY_ACCESSOR.putByte( cursor++, (byte) 0 );

        return (int) ( cursor - pointer );
    }

    /**
     * Reads a null-terminated string.
     *
     * @param pointer
     *            the start of the string
     *
     * @return a string
     */
    public static String decode( final long pointer )
    {
        if ( pointer == 0 )
        {
            return null;
        }

        // TODO v1.1 attempt it in Java for very short strings

        return NativeUnsafeAccess.readUtf8String( pointer );
    }

    /**
     * {@link FastCharset} implementation for {@link Utf8Coder}.
     *
     * @author Matthew Wilson
     */
    static final class Utf8FastCharset extends FastCharset
    {
        /** Creates a new instance. */
        Utf8FastCharset()
        {
            super( "UTF-8",
                   DniTypes.INTERNAL_STRING_PACKAGE + "/" + "Utf8Coder",
                   Charset.forName( "UTF-8" ),
                   1 );
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int encode( final String string, final long pointer )
        {
            return Utf8Coder.encode( string, pointer );
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String decode( final long pointer )
        {
            return Utf8Coder.decode( pointer );
        }
    }

}
